
這是「ERP 架構師筆記:定義驅動的框架設計」系列的 Day 1。
接下來三十天談的不是「怎麼用某個框架做一張表單」,而是怎麼設計一套能驅動上百張表單的框架:它該替使用它的人決定哪些事、哪些留給對方、每個機制為什麼長成那個樣子。案例固定用 Northwind 範例資料庫驗證,但案例是佐證,不是主角。
本篇說明:
我二十多年來一直在 ERP/HRM 套裝軟體業擔任軟體架構師。套裝軟體的產品週期非常長,動輒十年、二十年起跳,這段時間裡系統不只是被維護,還得跟著前端技術的世代更替長出不同的樣子,我經歷過的就有桌面程式、Web、行動 App 三代,每一代來的時候問題都一樣:那幾百張畫面,要不要整批重做一次?在這種產品線上,架構決策不會在三年後被誰重寫掉,你會在十年後親眼看到它長成什麼樣子,然後繼續維護它。而我一直待在同一個職位上,沒有轉去管專案、管團隊而離開程式碼,當年那些決定的後果,一次也沒有錯過。
這些年累積的想法,有些在專案裡做過,更多是想過而沒有動手,一個人要把它們全部寫出來並不划算,而這個成本現在不一樣了。我用 Claude Code 把沒動手的那些做出來,成了一套開源框架 bee-library,後面的 Northwind 案例就跑在上面。
案例本身是另一個 repo,bee-northwind-avalonia:框架那一個裝的是機制,案例這一個只有應用自己的定義檔與程式,框架以套件引用進來。想知道套用這套框架做一個應用實際要準備哪些定義檔、還要寫哪些程式碼,看案例這一個比看框架清楚。
框架還在持續發展,該收斂成機制的東西會陸續加進來,介面也會跟著改,破壞性變更會發生。現階段要直接引用它的套件得先把這件事算進去,也可以整份 clone 回去改成自己需要的樣子。
這個系列不是它的使用教學,框架在這裡只有一個角色:證明這些想法不是紙上談兵。三十篇談的是它已經做出來的那些機制,遇到什麼問題、怎麼解、代價是什麼、為什麼選這一種,實作細節不逐項展開。你完全可以不用它,只把「讓定義當源頭」這個想法帶走,用你熟悉的語言和工具去實現。
整個系列的命題只有一句:
ERP 的複雜度不在單一功能有多難,而在同一件事被重述太多次、乘以太多張表、再乘以太多次客製。
需求:訂單要多一個「預計到貨日」。沒有計算、沒有連動、不影響既有邏輯。使用者說這是「加一個欄位而已,應該很簡單吧」,而且他說得對。
在一套典型的 Code-First 堆疊(ORM + Web API + 前端)裡,這個欄位會落到十三個地方:
| # | 位置 | 內容 |
|---|---|---|
| 1 | Entity 類別 | 加一個屬性 |
| 2 | 型別對應設定 | 精度、允不允許 NULL、欄名對應 |
| 3 | Migration | 產生、審閱、進版控、上線時執行 |
| 4 | 查詢投影 | 清單查詢的 Select、報表的欄位清單 |
| 5 | DTO | Request 與 Response 各一次 |
| 6 | Mapper 設定 | Entity 與 DTO 的對應 |
| 7 | 驗證 | 必填、範圍、與訂單日期的先後關係 |
| 8 | API 契約 | OpenAPI schema,並重新發佈給消費端 |
| 9 | 前端型別 | TypeScript interface 或 C# model |
| 10 | 前端表單 | 控件、標籤、版面位置 |
| 11 | 前端清單 | 欄位、寬度、排序 |
| 12 | 多語系資源 | 每個語系一份 |
| 13 | 測試資料 | fixture、seed、假資料產生器 |
先說公道話:其中好幾處可以自動產生(Migration 由工具產生、OpenAPI 由組件掃描產生、前端型別可由 OpenAPI 產生),真正要手寫的大約五、六處。
但重點不在手寫幾處:
「預計到貨日是一個
Date、必填、顯示在訂單日期後面」這一個事實,在這套堆疊裡被重述了十三次。
每一次重述都是一個可能不一致的點:
這些不是「難」的問題,是「多」的問題,而且每一次不一致都要靠一次測試或一次使用者回報才會被發現。
第 1 到 8 項在後端,第 9 到 13 項在前端。前後端若是兩個團隊,那條線正好穿過表格中間,而跨過邊界的每一處性質都會改變:從「編輯」變成「協調」。
一個加欄位的需求於是長成這樣:
上面這幾件事,沒有一件是在寫那個欄位。它們吃掉的不是工時,是等待:使用者算的是從開口到上線過了多久,工程師算的是自己動手多久,於是一邊問「你們改一個欄位要這麼久?」,另一邊覺得自己並沒有偷懶。兩邊都對,因為成本是真的,只是不落在敲鍵盤那一段。
一套完整的套裝 ERP 涵蓋財務、採購、庫存、生產、銷售、人資,業務資料表動輒數千張。
ORM 的核心前提是物件與資料表的對應,所以:
數千張業務表
→ 數千個 Entity 類別
→ 加上 DTO(多半不只一個:List / Detail / Create / Update)
→ 加上 Mapper 設定
→ 加上 Repository
這些類別絕大多數彼此雷同。Supplier 和 Shipper 的 Entity 差在欄位名與數量,結構上是同一個模子。
這不是在說 ORM 不好。ORM 在它被設計的場景裡運作良好,問題出在數量:當同一個模式要被複製數千次,複製這件事本身就變成主要成本。
而關連把這個數量再放大一次:
把第一節那十三格乘以數千,連同那條團隊邊界一起乘。跨團隊協調不是每張表付一次,是每次需求付一次,而需求數量本身就跟表數量成正比。
這不是「麻煩」,是管不動。這些症狀,維護過大型系統的人都認得:
前兩層講「改起來多費工」,第三層講更根本的問題。這一層繞不開,因為:
ERP 幾乎不存在「裝了就用」這回事。
每次導入都伴隨客製:這家的單號規則不同、那家的成本計算方式不同、這個部門要多兩個欄位、那張報表要換一種分組。差別只在客製透過什麼路徑交付。
Code-First 模式下,那十三處全部是程式碼,而程式碼只有一條交付路徑:
改 → 編譯 → 測試 → 打包 → 部署
同一條路徑在三種部署形態下痛成三種樣子:
| 部署形態 | 客製的痛點 |
|---|---|
| 單一組織內部系統 | 最單純,排一次維護視窗即可。但「把小數位數從 2 改成 4」仍要走完整 build / test / deploy |
| 套裝賣 N 家,各自部署 | 只有兩條路:進主幹用開關控制(開關越積越多,最後沒人敢刪任何一個),或開分支(N 條分支各自演進,主幹修掉的問題要逐一同步) |
| 多租戶 SaaS,共用組件 | 租戶 A 改一個計算式,卻要協調所有租戶的維護視窗、回歸測試所有租戶的功能,任何一個租戶出事就整批 rollback |
三種形態,同一個結論:
需求的大小,與交付的成本,徹底脫鉤。
不管需求是「把小數位數從 2 改成 4」還是動到整個模組,走的都是同一條交付路徑、承擔同一份部署風險。結果是小需求也得排進版本節奏,工程團隊被迫把所有變更都當大事處理。
脫鉤的根源是客製被表達成了程式碼。部署形態只決定這件事有多痛,不決定它痛不痛。
這一層才是定義驅動要解的問題:
定義是資料,不是程式碼。改資料不需要重新編譯,也不需要重新部署組件。
前三節的成本收斂起來就是這三個:
| 老問題 | 症狀 |
|---|---|
| 規格分散三層 | 同一個欄位在 UI、DTO、DB Migration 各定義一次,極易不一致 |
| 業務邏輯分散 | 不同模組由不同工程師維護,風格不一、重複開發 |
| 客製化難標準化 | 客製邏輯無法回饋到標準版,累積成無法治理的技術債 |
過去二十年的解法多半是加工具:程式碼產生器、專案樣板、更聰明的 mapper、更自動的 migration。這些工具確實讓每一次重述變便宜。
但它們沒有改變「同一件事被重述十三次」這個結構,只是讓重述變快。
第三層的客製成本,工具更幫不上忙:產生出來的程式碼還是程式碼,還是要編譯部署。第三個老問題拖了二十年沒解,根源就在這裡。
問題不在工具,在典範。只有一個問題要回答:
系統的唯一真相,是程式碼,還是定義?
差別不只是檔案格式從 .cs 換成 .xml,而在源頭的性質:
| 程式碼是源頭 | 定義是源頭 | |
|---|---|---|
| 上千張表的表現形式 | 上千個類別 | 上千份定義資料 |
| 要處理它們,你能用 | 編譯器、IDE、重構工具 | 加上:批次腳本、產生器、程式化檢核、AI |
| 變更的交付路徑 | 編譯、打包、部署 | 換一份定義 |
| 客製的表現形式 | 分支或開關,散在原始碼裡 | 疊加在標準定義之上的一層資料 |
| 前端怎麼知道有哪些欄位 | 另一份手寫的型別,靠契約與人來同步 | 執行期取得同一份定義 |
| 換一代前端要付什麼 | 每張畫面重做一次 | 寫一個新的渲染層 |
| 誰能修改 | 開發者 | 開發者、顧問、工具,甚至使用者 |
其中兩列要多說幾句:
這也是版面要獨立成一層定義的理由:結構穩定,跟著業務走;版面易變,跟著前端世代走。分開之後,換代時重寫的是渲染層,不是那上千份結構定義。
回到「訂單加預計到貨日」。我拿本系列的案例應用實際數了一次,答案是三處定義修改,零行程式碼。
資料表結構:
<DbField FieldName="required_date" Caption="Required Date" DbType="Date" />
表單結構:
<FormField FieldName="required_date" Caption="Required Date" DbType="Date" />
版面:
<LayoutField FieldName="required_date" Caption="Required Date" />
改完之後自動跟上的有:資料庫欄位、表單控件、清單欄位、新增修改刪除的 SQL、跨層傳輸。
這個數字視情況會多一處或少一處,但重點不在數字,是這幾處全都是定義資料。
前端不再自己宣告一次欄位,它在執行期向後端要那份定義,然後照著渲染:
前端啟動 → 向後端要那份定義 → 依定義長出控件、清單欄、驗證
剩下的只有第一步的一半:這個欄位在業務上叫什麼、放哪裡、要不要必填。而這件事本來就該有人決定,它是業務判斷,不是溝通成本。
但這條邊界沒有消失,只是移動了。
還有兩個現實問題,框架都得處理掉:
重點只有一句:改定義到生效,中間沒有編譯、沒有打包、沒有部署組件。
| 章 | 主題 |
|---|---|
| 一(Day 1–3) | 定義驅動的起點 |
| 二(Day 4–8) | 定義層設計 |
| 三(Day 9–12) | 資料存取與快取 |
| 四(Day 13–18) | 業務邏輯與 API |
| 五(Day 19–21) | 多租戶與客製化 |
| 六(Day 22–25) | 傳輸、安全與稽核 |
| 七(Day 26–28) | 資料語意正確性 |
| 八(Day 29–30) | 案例回訪與全圖 |
每一篇的寫法大致相同:先講這件事要考量什麼,再談框架怎麼設計、為什麼這樣設計,能拿案例驗證的就用 Northwind 的實際定義片段收尾。
兩件事先講清楚:
明天談框架該替使用者決定哪些事,後天讓案例正式登場。
案例用 Northwind 做成一套完整的應用:八張表單、主檔明細、三處跨表關連、驗證都在裡面,而其中只有一張需要自己寫程式。那一張裡裝了什麼、以及有哪些地方我們選擇不走定義,系列收尾前會把整套應用攤開來逐項對帳。
ERP 的敵人不是複雜,是重述。而重述之所以無法用工具消滅,是因為它是典範的產物。只要程式碼還是源頭,同一個事實就注定要被說很多次。
本系列同步發表於 HackMD,完整目錄
把「預計到貨日」從 UI、DTO 到 DB 要重述那麼多次,真的一看就懂為什麼 ERP 會卡在重複上;你把同一欄位在 Code-First 裡要碰十三處、到 Definition-Driven 只剩三個定義檔,差距很有感。還有那條前後端團隊邊界,從每次需求協調變成只管渲染層,這個切法也很俐落。手邊有多的 Lovable 額度想送給有緣人,有興趣可從連結看看我的系列。 https://ithelp.ithome.com.tw/articles/10401174